﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;

using PickGold.Data.Hsts.Records;
using PickGold.Data.Hsts.Database;
using PickGold.Data.Hsts.General.Compression;
using PickGold.Data.Hsts.General.Persist;
using System.Threading;

namespace PickGold.Data.Hsts.WaterfallTree
{
	public class Locator : IDescriptor, IComparable<Locator>, IEquatable<Locator>
	{
		public const byte VERSION = 40;

		private byte[] serializationData;

		private int hashCode;

		private bool isDeleted;

		private string name;

		private Type keyType;
		private Type recordType;

		private Dictionary<string, int> keyMembers;
		private Dictionary<string, int> recordMembers;

		private IComparer<IData> keyComparer;
		private IEqualityComparer<IData> keyEqualityComparer;
		private IPersist<IData> keyPersist;
		private IPersist<IData> recordPersist;
		private IIndexerPersist<IData> keyIndexerPersist;
		private IIndexerPersist<IData> recordIndexerPersist;

		private DateTime createTime;
		private DateTime modifiedTime;
		private DateTime accessTime;

		private byte[] tag;

		public readonly object SyncRoot = new object();

		internal static readonly Locator MIN;

		static Locator()
		{
			MIN = new Locator(0, null, PickGold.Data.Hsts.Database.StructureType.RESERVED, DataType.Boolean, DataType.Boolean, null, null);
			MIN.keyPersist = SentinelPersistKey.Instance;
		}

		public IOperationCollectionFactory OperationCollectionFactory;
		public IDataContainerFactory DataContainerFactory;

		public Locator(long id, string name, int structureType, DataType keyDataType, DataType recordDataType, Type keyType, Type recordType)
		{
			if (keyDataType == null)
				throw new ArgumentException("keyDataType");
			if (recordDataType == null)
				throw new ArgumentException("recordDataType");

			ID = id;
			Name = name;
			StructureType = structureType;

			hashCode = ID.GetHashCode();

			//apply
			switch (structureType)
			{
				case PickGold.Data.Hsts.Database.StructureType.XTABLE: Apply = new XTableApply(this); break;
				case PickGold.Data.Hsts.Database.StructureType.XFILE: Apply = new XStreamApply(this); break;
			}

			KeyDataType = keyDataType;
			RecordDataType = recordDataType;

			KeyType = keyType;
			RecordType = recordType;

			CreateTime = DateTime.Now;
			ModifiedTime = CreateTime;
			AccessTime = CreateTime;

			OperationCollectionFactory = new OperationCollectionFactory(this);
			DataContainerFactory = new DataContainerFactory(this);
		}

		private void WriteMembers(BinaryWriter writer, Dictionary<string, int> members)
		{
			if (members == null)
			{
				writer.Write(false);
				return;
			}

			writer.Write(true);
			writer.Write(members.Count);

			foreach (var kv in members)
			{
				writer.Write(kv.Key);
				writer.Write(kv.Value);
			}
		}

		private static Dictionary<string, int> ReadMembers(BinaryReader reader)
		{
			if (!reader.ReadBoolean())
				return null;

			int count = reader.ReadInt32();
			Dictionary<string, int> members = new Dictionary<string, int>(count);

			for (int i = 0; i < count; i++)
			{
				string key = reader.ReadString();
				int value = reader.ReadInt32();

				members.Add(key, value);
			}

			return members;
		}

		private void InternalSerialize(BinaryWriter writer)
		{
			if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
				Fiber.ThrowLockTimeoutException(SyncRoot);
			try
			{
				writer.Write(VERSION);

				writer.Write(ID);
				if (ID == Locator.MIN.ID)
					return;

				writer.Write(IsDeleted);

				writer.Write(Name);
				writer.Write(checked((byte)StructureType));

				//data types
				KeyDataType.Serialize(writer);
				RecordDataType.Serialize(writer);

				//types
				if (!DataTypeUtils.IsAnonymousType(KeyType))
					writer.Write(KeyType.FullName);
				else
					writer.Write("");

				if (!DataTypeUtils.IsAnonymousType(RecordType))
					writer.Write(RecordType.FullName);
				else
					writer.Write("");

				//key & record members
				WriteMembers(writer, keyMembers);
				WriteMembers(writer, recordMembers);

				//times
				writer.Write(CreateTime.Ticks);
				writer.Write(ModifiedTime.Ticks);
				writer.Write(AccessTime.Ticks);

				//tag
				if (Tag == null)
				{
					writer.Write(false);
				}
				else
				{
					writer.Write(true);
					CountCompression.Serialize(writer, (ulong)Tag.Length);
					writer.Write(Tag);
				}
			}
			finally
			{
				Monitor.Exit(SyncRoot);
			}
		}

		public void Serialize(BinaryWriter writer)
		{
			if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
				Fiber.ThrowLockTimeoutException(SyncRoot);
			try
			{
				if (serializationData == null)
				{
					using (MemoryStream ms = new MemoryStream())
					{
						InternalSerialize(new BinaryWriter(ms));
						serializationData = ms.ToArray();
					}
				}

				writer.Write(serializationData);
			}
			finally
			{
				Monitor.Exit(SyncRoot);
			}
		}

		public static Locator Deserialize(BinaryReader reader)
		{
			if (reader.ReadByte() != VERSION)
				throw new Exception("Invalid Locator version.");

			long id = reader.ReadInt64();
			if (id == Locator.MIN.ID)
				return Locator.MIN;

			bool isDeleted = reader.ReadBoolean();

			string name = reader.ReadString();
			int structureType = reader.ReadByte();

			//data types
			DataType keyDataType = DataType.Deserialize(reader);
			DataType recordDataType = DataType.Deserialize(reader);

			//types
			string sKeyType = reader.ReadString();
			Type keyType = (sKeyType != "") ? TypeCache.GetType(sKeyType) : DataTypeUtils.BuildType(keyDataType);

			string sRecordType = reader.ReadString();
			Type recordType = (sRecordType != "") ? TypeCache.GetType(sRecordType) : DataTypeUtils.BuildType(recordDataType);

			//key & record members
			var keyMembers = ReadMembers(reader);
			var recordMembers = ReadMembers(reader);

			//create time
			DateTime createTime = new DateTime(reader.ReadInt64());
			DateTime modifiedTime = new DateTime(reader.ReadInt64());
			DateTime accessTime = new DateTime(reader.ReadInt64());

			//tag
			byte[] tag = reader.ReadBoolean() ? reader.ReadBytes((int)CountCompression.Deserialize(reader)) : null;

			var locator = new Locator(id, name, structureType, keyDataType, recordDataType, keyType, recordType);

			locator.IsDeleted = isDeleted;

			locator.keyMembers = keyMembers;
			locator.recordMembers = recordMembers;

			locator.CreateTime = createTime;
			locator.ModifiedTime = modifiedTime;
			locator.AccessTime = accessTime;

			locator.Tag = tag;

			return locator;
		}

		public bool IsReady { get; private set; }

		private TypeEngine keyEngine;
		private TypeEngine recEngine;

		private void DoPrepare()
		{
			Debug.Assert(KeyType != null);
			Debug.Assert(RecordType != null);

			//keys
			if (KeyComparer == null || KeyEqualityComparer == null || (KeyPersist == null || KeyIndexerPersist == null))
			{
				if (keyEngine == null)
					keyEngine = TypeEngine.Default(KeyType, true);

				if (KeyComparer == null)
					KeyComparer = keyEngine.Comparer;

				if (KeyEqualityComparer == null)
					KeyEqualityComparer = keyEngine.EqualityComparer;

				if (KeyPersist == null)
					KeyPersist = keyEngine.Persist;

				if (KeyIndexerPersist == null)
					KeyIndexerPersist = keyEngine.IndexerPersist;
			}

			//records
			if (RecordPersist == null || RecordIndexerPersist == null)
			{
				if (recEngine == null)
					recEngine = TypeEngine.Default(RecordType, false);

				if (RecordPersist == null)
					RecordPersist = recEngine.Persist;

				if (RecordIndexerPersist == null)
					RecordIndexerPersist = recEngine.IndexerPersist;
			}

			//container
			if (DataContainerPersist == null)
			{
				if (KeyIndexerPersist != null && RecordIndexerPersist != null)
					DataContainerPersist = new DataContainerPersist(KeyIndexerPersist, RecordIndexerPersist, DataContainerFactory);
				else
					DataContainerPersist = new DataContainerPersist(KeyPersist, RecordPersist, DataContainerFactory);
			}

			//operations
			if (OperationsPersist == null)
				OperationsPersist = new OperationCollectionPersist(KeyPersist, RecordPersist, OperationCollectionFactory);

			IsReady = true;
		}

		public IApply Apply { get; private set; }
		public IPersist<IDataContainer> DataContainerPersist { get; private set; }
		public IPersist<IOperationCollection> OperationsPersist { get; private set; }

		public int CompareTo(Locator other)
		{
			return this.ID.CompareTo(other.ID);
		}

		public bool Equals(Locator other)
		{
			return this.ID == other.ID;
		}

		public override bool Equals(object obj)
		{
			if (!(obj is Locator))
				return false;

			return Equals((Locator)obj);
		}

		public override int GetHashCode()
		{
			return hashCode;
		}

		public override string ToString()
		{
			return Name;
		}

		public static bool operator ==(Locator x, Locator y)
		{
			bool xNotNull = !Object.ReferenceEquals(x, null);
			bool yNotNull = !Object.ReferenceEquals(y, null);

			if (xNotNull && yNotNull)
				return x.Equals(y);

			if (xNotNull || yNotNull)
				return false;

			return true;
		}

		public static bool operator !=(Locator x, Locator y)
		{
			return !(x == y);
		}

		public static implicit operator string(Locator locator)
		{
			return locator.ToString();
		}

		public bool IsDeleted
		{
			get
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					return isDeleted;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
			set
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					if (value != isDeleted)
					{
						isDeleted = value;
						serializationData = null;
					}
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
		}

		#region IDescription

		public long ID { get; private set; }

		public string Name
		{
			get { return name; }
			set
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					name = value;
					serializationData = null;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
		}

		public int StructureType { get; private set; }

		public DataType KeyDataType { get; private set; }
		public DataType RecordDataType { get; private set; }

		public Type KeyType
		{
			get { return keyType; }
			set
			{
				if (keyType == value)
					return;

				if (value != null && DataTypeUtils.BuildDataType(value) != KeyDataType)
					throw new Exception(String.Format("The type {0} is not compatible with anonymous type {1}.", value, KeyDataType));

				keyType = value;
				keyEngine = null;

				keyComparer = null;
				keyEqualityComparer = null;
				keyPersist = null;
				keyIndexerPersist = null;

				DataContainerPersist = null;
				OperationsPersist = null;

				IsReady = false;
			}
		}

		public Type RecordType
		{
			get { return recordType; }
			set
			{
				if (recordType == value)
					return;

				if (value != null && DataTypeUtils.BuildDataType(value) != RecordDataType)
					throw new Exception(String.Format("The type {0} is not compatible with anonymous type {1}.", value, RecordDataType));

				recordType = value;
				recEngine = null;

				recordPersist = null;
				recordIndexerPersist = null;

				DataContainerPersist = null;
				OperationsPersist = null;

				IsReady = false;
			}
		}

		public IComparer<IData> KeyComparer
		{
			get
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					return keyComparer;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
			set
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					keyComparer = value;
					IsReady = false;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
		}

		public IEqualityComparer<IData> KeyEqualityComparer
		{
			get
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					return keyEqualityComparer;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
			set
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					keyEqualityComparer = value;
					IsReady = false;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
		}

		public IPersist<IData> KeyPersist
		{
			get
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					return keyPersist;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
			set
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					keyPersist = value;

					DataContainerPersist = null;
					OperationsPersist = null;

					IsReady = false;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
		}

		public IPersist<IData> RecordPersist
		{
			get
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					return recordPersist;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
			set
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					recordPersist = value;

					DataContainerPersist = null;
					OperationsPersist = null;

					IsReady = false;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
		}

		public IIndexerPersist<IData> KeyIndexerPersist
		{
			get
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					return keyIndexerPersist;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
			set
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					keyIndexerPersist = value;
					DataContainerPersist = null;

					IsReady = false;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
		}

		public IIndexerPersist<IData> RecordIndexerPersist
		{
			get
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					return recordIndexerPersist;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
			set
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					recordIndexerPersist = value;
					DataContainerPersist = null;

					IsReady = false;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
		}

		public void Prepare()
		{
			if (IsReady)
				return;

			if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
				Fiber.ThrowLockTimeoutException(SyncRoot);
			try
			{
				DoPrepare();
			}
			finally
			{
				Monitor.Exit(SyncRoot);
			}
		}

		public DateTime CreateTime
		{
			get
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					return createTime;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
			set
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					createTime = value;
					serializationData = null;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
		}

		public DateTime ModifiedTime
		{
			get
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					return modifiedTime;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
			set
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					modifiedTime = value;
					serializationData = null;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
		}

		public DateTime AccessTime
		{
			get
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					return accessTime;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
			set
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					accessTime = value;
					serializationData = null;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
		}

		public byte[] Tag
		{
			get
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					return tag;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
			set
			{
				if (!Monitor.TryEnter(SyncRoot, Fiber.LockTimeout))
					Fiber.ThrowLockTimeoutException(SyncRoot);
				try
				{
					tag = value;
					serializationData = null;
				}
				finally
				{
					Monitor.Exit(SyncRoot);
				}
			}
		}

		#endregion
	}
}
